/*            Copyright(c) 2006-2008 Dave Sexton           *
 * http://www.codeplex.com/DocProject                      *
 * http://www.codeplex.com/DocProject/Project/License.aspx *
 ***********************************************************/
using System;
using System.Collections.Generic;
using System.Text;
using VSLangProj80;
using EnvDTE;
using System.IO;
using VSLangProj;
using Microsoft.VisualStudio.VCProjectEngine;
using System.Text.RegularExpressions;
using DaveSexton.DocProject.Resources;

namespace DaveSexton.DocProject
{
	[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#")]
	public delegate bool MacroEvaluator(string macro, string name, out string value);

	public class VCProjectOutput : MarshalByRefObject, IProjectOutput
	{
		#region Public Properties
		public virtual string RelativeOutputPath
		{
			get
			{
				string path = EvaluateMacros(ActiveConfiguration.OutputDirectory);

				if (string.IsNullOrEmpty(path))
					throw new InvalidOperationException(Errors.NoOutputPath);
				else if (Path.IsPathRooted(path))
				{
					Uri root = new Uri(project.ProjectDirectory);

					return PathFormat.MakeLocal(System.Web.HttpUtility.UrlDecode(
						root.MakeRelativeUri(new Uri(path)).ToString()));
				}
				else
					return PathFormat.MakeLocal(path);
			}
		}

		public string OutputPath
		{
			get
			{
				string path = RelativeOutputPath;

				if (Path.IsPathRooted(path))
					return path;
				else
					return Path.Combine(project.ProjectDirectory, path);
			}
		}

		public virtual string OutputFileName
		{
			get
			{
				return Path.GetFileName(ActiveConfiguration.PrimaryOutput);
			}
		}

		public virtual SystemPath OutputFile
		{
			get
			{
				return new SystemPath(Path.Combine(OutputPath, OutputFileName), false, false);
			}
		}

		public virtual SystemPath XmlDocumentationFile
		{
			get
			{
				VCXDCMakeTool tool = GetTool<VCXDCMakeTool>();

				if (tool == null)
					return SystemPath.Empty;
				else
				{
					string file = EvaluateMacros(tool.OutputDocumentFile);

					return (string.IsNullOrEmpty(file)) ? SystemPath.Empty : new SystemPath(file, false, false);
				}
			}
		}
		#endregion

		#region Private / Protected
		[CLSCompliant(false)]
		protected VCConfiguration ActiveConfiguration
		{
			get
			{
				VSProject2 project2 = project as VSProject2;

				if (project2 != null)
					return ActiveConfigurationInHost;
				else
				// VCProject does not implement VSProject2 when it is constructed using the VCProjectEngineObjectClass.LoadProject method, 
				// although it does implement VSProject2 when a reference to the VCProject is obtained through Visual Studio automation using 
				// the DTE object.
				{
					IVCCollection configurations = (IVCCollection) project.Configurations;
					VCConfiguration bestMatch = null;

					string activeConfig = source.Properties["Configuration"];
					string activePlatform = source.Properties["Platform"];

					if (!string.IsNullOrEmpty(activeConfig))
					{
						foreach (VCConfiguration configuration in configurations)
						{
							if (bestMatch == null && activeConfig.Equals(configuration.ConfigurationName, StringComparison.OrdinalIgnoreCase))
								bestMatch = configuration;

							if (activePlatform == null || activePlatform.Equals(((VCPlatform) configuration.Platform).Name))
								break;
						}
					}

					return bestMatch ?? ((configurations.Count == 0) ? null : (VCConfiguration) configurations.Item(0));
				}
			}
		}

		private VCConfiguration ActiveConfigurationInHost
		{
			get
			{
				// When this code was defined in the ActiveConfiguration property during testing, MSBuild threw the following exception
				// if "project" was loaded explicitly by the MSBuildVCProjectOutput class and passed to the constructor that takes a 
				// match evaluator instead of being referenced in the IDE, where the exception does not occur, using any of the other 
				// constructor overloads. 
				//
				// System.IO.FileNotFoundException: Could not load file or assembly 'EnvDTE, Version=7.0.3300.0, 
				//   Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' or one of its dependencies. The system cannot find the file specified.
				// File name: 'EnvDTE, Version=7.0.3300.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'
				//
				// This file is not located anywhere on the development machine.  The latest version, 8.0.0.0, is in the GAC.
				Project projectBase = ((VSProject2) project).Project;
				string name = projectBase.ConfigurationManager.ActiveConfiguration.ConfigurationName;

				IVCCollection configurations = (IVCCollection) project.Configurations;

				return (VCConfiguration) configurations.Item(name);
			}
		}

		private static readonly Regex macro = new Regex(@"\$\(([^\)]+)\)", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
		private readonly VCProject project;
		private readonly MacroEvaluator macroEvaluator;
		private readonly IAnyProject source;
		#endregion

		#region Constructors
		/// <summary>
		/// Constructs a new instance of the <see cref="VCProjectOutput" /> class.
		/// </summary>
		[CLSCompliant(false)]
		public VCProjectOutput(IAnyProject source, VCProject project, MacroEvaluator macroEvaluator)
		{
			if (source == null)
				throw new ArgumentNullException("source");

			if (project == null)
				throw new ArgumentNullException("project");

			this.source = source;
			this.project = project;
			this.macroEvaluator = macroEvaluator;
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="VCProjectOutput" /> class.
		/// </summary>
		public VCProjectOutput(VCSourceProject project)
		{
			if (project == null)
				throw new ArgumentNullException("project");

			this.project = project.VCProject;
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="VCProjectOutput" /> class.
		/// </summary>
		public VCProjectOutput(VCAnyProject project)
		{
			if (project == null)
				throw new ArgumentNullException("project");

			this.project = (VCProject) project.VSProject;
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="VCProjectOutput" /> class.
		/// </summary>
		public VCProjectOutput(VCDocProject project)
		{
			if (project == null)
				throw new ArgumentNullException("project");

			this.project = (VCProject) project.VSProject;
		}
		#endregion

		#region Methods
		/// <summary>
		/// See <see href="http://msdn2.microsoft.com/en-us/library/c02as0cs(VS.71).aspx">Macros for Build Commands and Properties</see> 
		/// for more information.
		/// </summary>
		public string EvaluateMacros(string path)
		{
			if (macroEvaluator != null)
				return EvaluateMacros(path, macroEvaluator);

			return macro.Replace(path, delegate(Match match)
			{
				return EvaluateMacro(match.Value, match.Groups[1].Value);
			});
		}

		/// <summary>
		/// See <see href="http://msdn2.microsoft.com/en-us/library/c02as0cs(VS.71).aspx">Macros for Build Commands and Properties</see> 
		/// for more information.
		/// </summary>
		public string EvaluateMacros(string path, MacroEvaluator evaluator)
		{
			return macro.Replace(path, delegate(Match match)
			{
				string value;

				if (evaluator(match.Value, match.Groups[1].Value, out value))
					return value;
				else
					return EvaluateMacro(match.Value, match.Groups[1].Value);
			});
		}

		/// <summary>
		/// See <see href="http://msdn2.microsoft.com/en-us/library/c02as0cs(VS.71).aspx">Macros for Build Commands and Properties</see> 
		/// for more information.
		/// </summary>
		protected virtual string EvaluateMacro(string macro, string name)
		{
			VCConfiguration config = ActiveConfiguration;

			switch (name)
			{
				case "ConfigurationName":
					return config.ConfigurationName;
				case "PlatformName":
					return ((VCPlatform) config.Platform).Name;
				case "IntDir":
					return PathFormat.EnsureDirectory(config.IntermediateDirectory);
				case "OutDir":
					return PathFormat.EnsureDirectory(config.OutputDirectory);
				case "TargetDir":
					return PathFormat.EnsureDirectory(OutputPath);
				case "TargetPath":
					return OutputFile.Path;
				case "TargetName":
					return Path.GetFileNameWithoutExtension(OutputFileName);
				case "TargetFileName":
					return OutputFileName;
				case "TargetExt":
					return Path.GetExtension(OutputFileName);
				case "DevEnvDir":
				case "InputDir":
				case "ProjectDir":
				case "SolutionDir":
				case "VSInstallDir":
				case "VCInstallDir":
				case "FrameworkDir":
				case "FrameworkSDKDir":
				case "WebDeployPath":
				case "WebDeployRoot":
				case "FxCopDir":
					return PathFormat.EnsureDirectory(((VCProjectEngine) project.VCProjectEngine).Evaluate(macro));
				default:
					return ((VCProjectEngine) project.VCProjectEngine).Evaluate(macro);
			}
		}

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate"), 
		System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter")]
		protected T GetTool<T>()
		{
			foreach (object tool in (IVCCollection) ActiveConfiguration.Tools)
			{
				if (tool is T)
					return (T) tool;
			}

			return default(T);
		}

		[System.Security.Permissions.SecurityPermission(System.Security.Permissions.SecurityAction.LinkDemand,
			Flags = System.Security.Permissions.SecurityPermissionFlag.Infrastructure)]
		public override object InitializeLifetimeService()
		{
			return null;
		}

		public override string ToString()
		{
			return "{" + OutputFileName + ", " + XmlDocumentationFile.Path + "}";
		}
		#endregion
	}
}
